// ----------------------------------------------------------------------------
// Copyright 2006, Martin D. Flynn
// All rights reserved
// ----------------------------------------------------------------------------
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------------------------------------------------------
// Description:
//  This class is the specific implementation of a GPS device which is accessible
//  via an 'rfcomm' serial connection ('rfcm' device).
// ----------------------------------------------------------------------------
// Change History:
//  2006/03/26  Martin D. Flynn
//      Initial release
//  2006/03/31  Martin D. Flynn
//      Cloned from "org.opendmtp.j2me.client.gps.rfcm.GPSDeviceImpl"
//  2006/04/02  Martin D. Flynn
//      'openDevice()' now does not throw an exception, but rather just returns 
//      false when unable to open a GPS connection.
// ----------------------------------------------------------------------------
package org.opendmtp.j2me.client.custom.treo650;

import java.io.IOException;
import java.io.InputStream;
import java.io.DataInputStream;
import java.io.InterruptedIOException;

import javax.microedition.io.CommConnection;
import javax.microedition.io.Connector;

import org.opendmtp.j2me.client.gps.GPSDevice;
import org.opendmtp.j2me.client.gps.GPSException;
import org.opendmtp.j2me.util.Log;

/**
 * Implements a GPS device which is accessible via an 'rfcomm' serial connection ('rfcm' device).
 * 
 * @author Martin D. Flynn
 * @author Pavel V. Senin
 * 
 */

public class GPSDeviceImpl implements GPSDevice {

  // ----------------------------------------------------------------------------
  /**
   * Defines device specific name.
   */
  private static final String LOG_NAME = "RFCM";

  // ----------------------------------------------------------------------------
  // PalmOS error codes:
  // http://www.palmos.com/dev/support/docs/palmos/PalmOSReference/ErrorCodes.html
  // System.getProperty("microedition.commports") <-- list all comports
  // Connector.open("comm:rfcm;baudrate=4800;bitsperchar=8;stopbits=1;parity=none")

  /**
   * Defines logical serial port connection.
   */
  private CommConnection connection = null;
  /**
   * Defines input stream id.
   */
  private DataInputStream inputStream = null;

  /**
   * Instantiates GPS device class by calling generic GPSDevice class constructor.
   * 
   * @throws GPSException when error occured.
   */
  public GPSDeviceImpl() throws GPSException {
    super();
  }

  /**
   * Opens serial connection to GPS unit.
   * 
   * @return the connection.
   * @throws IOException if got an error.
   */
  private CommConnection _openConnection() throws IOException {
    if (this.connection == null) {
      Log.setMessage(0, "Open GPS 'rfcm' ...");
      String url = "comm:rfcm;baudrate=4800;bitsperchar=8;stopbits=1;parity=none";
      Log.debug(LOG_NAME, "URL] " + url);
      this.connection = (CommConnection) Connector.open(url, Connector.READ_WRITE, true);
      // Log.setMessage(0, "GPS 'rfcm' is open ...");
      // Palm error codes:
      // 773
      // 775
    }
    return this.connection;
  }

  /**
   * Opens GPS device connection.
   * 
   * @return connection status, true if connected, false if unable to connect.
   * @throws GPSException if unable to open.
   */
  public boolean openDevice() throws GPSException {
    try {
      this._openConnection();
      return true;
    }
    catch (IOException ioe) {
      Log.error(LOG_NAME, "Unable to open GPS connection", ioe);
      return false; // throw new GPSException("IOException", ioe);
    }
  }

  /**
   * Reports the status of connection.
   * 
   * @return connection status, true if the device is opened, false if closed.
   */
  public boolean isOpen() {
    return (this.connection != null);
  }

  /**
   * Returns the input stream from GPS device.
   * 
   * @return input stream that could be read.
   * @throws GPSException if something got wrong.
   */
  private InputStream _getInputStream() throws GPSException {
    if (this.inputStream == null) {
      try {
        CommConnection comm = this._openConnection();
        this.inputStream = comm.openDataInputStream();
      }
      catch (Throwable t) {
        throw new GPSException("Exception", t);
      }
    }
    return this.inputStream;
  }

  /**
   * Defines a pause to wait for any data.
   */
  private static final long READ_CHAR_WAIT = 300L; // at 4800bps, 300 msec == ~144 characters.

  /**
   * Reads char from input stream.
   * 
   * @param dis input stream to read from.
   * @param timeoutMS defines timeout.
   * @return next byte from input stream.
   * @throws GPSException if GPS timeout encountered.
   * @throws InterruptedIOException if got IO exception.
   * @throws IOException if got IO exception.
   * @throws InterruptedException if got interrupted exception.
   */
  private int _readChar(InputStream dis, long timeoutMS) throws GPSException,
      InterruptedIOException, IOException, InterruptedException {
    // This method could use better optimization
    // Note that if the platform does not support the 'available' method, then
    // it will likely always return '0;, and this available test cannot be used.
    long accumTimerMS = 0L;
    while (dis.available() <= 0) {
      // nothing to read, wait a few milliseconds
      try {
        Thread.sleep(READ_CHAR_WAIT);
      }
      catch (Throwable t) {
      }
      accumTimerMS += READ_CHAR_WAIT;
      if (accumTimerMS > timeoutMS) {
        throw new GPSException("Read timeout!!");
      }
    }
    // finally, read the character
    return dis.read();
  }

  /**
   * Reads data line (string terminated by '\n' or '\r' symbols) from the GPS to the buffer.
   * 
   * @param sb buffer to put the line.
   * @param timeoutMS time to wait until timeout.
   * @return number of bytes that were read.
   * @throws GPSException if GPS error encountered.
   * @throws SecurityException if got security exception.
   * @throws IOException if got IO exception.
   * @throws InterruptedException if got interrupted exception.
   * 
   */
  public int readLine(StringBuffer sb, long timeoutMS) throws GPSException, InterruptedException,
      IOException, SecurityException {

    /* get input stream */
    InputStream dis = this._getInputStream();
    if (dis == null) {
      throw new GPSException("Bluetooth is not connected");
    }

    /* read loop */
    int count = 0;
    for (;;) {
      int ch = this._readChar(dis, timeoutMS); // timeout per character?
      if (ch < 0) {
        throw new GPSException("Read error (EOF?)");
      }
      else if (ch == 0) {
        // ignore
      }
      else if ((ch == '\n') || (ch == '\r')) {
        if (count > 0) {
          // end of line
          return count;
        }
        else {
          // continue reading
        }
      }
      else {
        sb.append((char) ch);
        count++;
      }
    }

  }

  /**
   * Closes device connection.
   */
  public void closeDevice() {
    if (this.inputStream != null) {
      try {
        this.inputStream.close();
      }
      catch (Throwable t) {
      }
      this.inputStream = null;
    }
    if (this.connection != null) {
      try {
        this.connection.close();
      }
      catch (Throwable t) {
      }
      this.connection = null;
    }
  }
}
